/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package backend;

import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.*;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 *
 * @author XpGG
 *
 * Only supports post scoring at the moment
 *
 */
public class Scores extends TimerTask {

    private String initQuery = "UPDATE user SET prof_vector = ";
    private String initString = null;
    private Connection connection = null;
    private Statement initStmt, calStmt = null;
    private int psid_old = -1, psid_new = -1;
    private int gsid_old = -1, gsid_new = -1;
    // following is the necessary data fetched from database
    private HashMap<Integer, User> users = null;
    private HashMap<Integer, Post> posts = null;
    private HashMap<Integer, Galaxy> galaxies = null;
    private HashMap<Integer, Nebula> nebulas = null;
    private LinkedList<PostScore> post_scores = null;
    private LinkedList<GalaxyScore> galaxy_scores = null;
    private Iterator<PostScore> ps_iterator;
    private Iterator<GalaxyScore> gs_iterator;
    private HashSet<User> m_users = new HashSet<>();

    public Scores() {
        initString = "";
        for (int i = 0; i < 200; i++) {
            initString += "1 ";
        }
        initQuery += "'" + initString + "' WHERE prof_vector = '0'";
    }

    private boolean initializeData() throws SQLException {
        if (connection == null || connection.isClosed()) {
            connection = SqlConnection.getInstance().getConnection();
            initStmt = connection.createStatement();
        }
        initializeUserPV();
        initializeNebulas();
        initializeGalaxies();
        initializeUsers();
        initializePosts();
        initializePostScores();
        initializeGalaxyScores();
        System.out.println("\nUsers - " + users.size() + " Posts - "
                + posts.size() + " Nebulas - " + nebulas.size()
                + " post_scores - " + post_scores.size()
                + " galaxy_scores - " + galaxy_scores.size());
        return true;
    }

    private void initializeUserPV() throws SQLException {
        if (connection == null || connection.isClosed()) {
            connection = SqlConnection.getInstance().getConnection();
            initStmt = connection.createStatement();
        }
        int num = initStmt.executeUpdate(initQuery);
        if (num > 0) {
            System.out.println("\n" + Calendar.getInstance().getTime() + ": " + num + " users's professional vectors have been initialized");
        }
    }

    private void initializeUsers() throws SQLException {
        if (connection == null || connection.isClosed()) {
            connection = SqlConnection.getInstance().getConnection();
            initStmt = connection.createStatement();
        }
        ResultSet rs;
        rs = initStmt.executeQuery("SELECT uid, prof_vector, GROUP_CONCAT( nid ) AS nids FROM user LEFT JOIN user_nebula USING( uid ) GROUP BY uid");
        users = new HashMap<>();
        while (rs.next()) {
            User user = User.CreateUser(rs);
            users.put(user.uid, user);
        }
    }

    private void initializePosts() throws SQLException {
        if (connection == null || connection.isClosed()) {
            connection = SqlConnection.getInstance().getConnection();
            initStmt = connection.createStatement();
        }
        ResultSet rs;
        posts = new HashMap<>();
        String query = "SELECT pid, post.uid AS uid, default_nid, GROUP_CONCAT( nid ) AS nids, t_score, pos_count, neg_count "
                + "FROM post "
                + "LEFT JOIN post_nebula USING( pid ) "
                + "LEFT JOIN ( SELECT pid, COUNT( pid ) AS pos_count FROM post_score WHERE score > 0 GROUP BY pid) pos USING( pid ) "
                + "LEFT JOIN ( SELECT pid, COUNT( pid ) AS neg_count FROM post_score WHERE score < 0 GROUP BY pid) neg USING( pid ) "
                + "GROUP BY pid";
        rs = initStmt.executeQuery(query);
        while (rs.next()) {
            Post post = Post.CreatePost(rs);
            posts.put(post.pid, post);
        }
    }

    private void initializeGalaxies() throws SQLException {
        if (connection == null || connection.isClosed()) {
            connection = SqlConnection.getInstance().getConnection();
            initStmt = connection.createStatement();
        }
        ResultSet rs;
        galaxies = new HashMap<>();
        String query = "SELECT gid, galaxy.uid AS uid, GROUP_CONCAT( nid ) AS nids, pos_count, neg_count "
                + "FROM galaxy "
                + "LEFT JOIN galaxy_nebula USING( gid ) "
                + "LEFT JOIN ( SELECT gid, COUNT( gid ) AS pos_count FROM galaxy_score WHERE score > 0 GROUP BY gid) pos USING( gid ) "
                + "LEFT JOIN ( SELECT gid, COUNT( gid ) AS neg_count FROM galaxy_score WHERE score < 0 GROUP BY gid) neg USING( gid ) "
                + "GROUP BY gid";
        rs = initStmt.executeQuery(query);
        while (rs.next()) {
            Galaxy galaxy = Galaxy.CreateGalaxy(rs);
            galaxies.put(galaxy.gid, galaxy);
        }
    }

    private boolean initializePostScores() throws SQLException {
        if (connection == null || connection.isClosed()) {
            connection = SqlConnection.getInstance().getConnection();
            initStmt = connection.createStatement();
        }
        ResultSet rs;
        rs = initStmt.executeQuery("SELECT * FROM post_score_log ORDER BY pslid DESC LIMIT 0, 1");
        if (rs.next()) {
            psid_old = rs.getInt("psid");
        } else {
            psid_old = 0;
        }

        rs = initStmt.executeQuery("SELECT uid, pid, psid, score FROM post_score WHERE psid > " + psid_old + " ORDER BY psid");
        post_scores = new LinkedList<>();
        while (rs.next()) {
            PostScore score = PostScore.CreateScore(rs);
            post_scores.add(score);
            m_users.add(users.get(posts.get(score.pid).uid));
        }
        if (post_scores.isEmpty()) {
            return false;
        } else {
            psid_new = post_scores.getLast().psid;
            ps_iterator = post_scores.iterator();
            return true;
        }
    }

    private boolean initializeGalaxyScores() throws SQLException {
        if (connection == null || connection.isClosed()) {
            connection = SqlConnection.getInstance().getConnection();
            initStmt = connection.createStatement();
        }
        ResultSet rs;
        rs = initStmt.executeQuery("SELECT * FROM galaxy_score_log ORDER BY gslid DESC LIMIT 0, 1");
        if (rs.next()) {
            gsid_old = rs.getInt("gsid");
        } else {
            gsid_old = 0;
        }

        rs = initStmt.executeQuery("SELECT uid, gid, gsid, score FROM galaxy_score WHERE gsid > " + gsid_old + " ORDER BY gsid");
        galaxy_scores = new LinkedList<>();
        while (rs.next()) {
            GalaxyScore score = GalaxyScore.CreateScore(rs);
            galaxy_scores.add(score);
            m_users.add(users.get(galaxies.get(score.gid).uid));
        }
        if (galaxy_scores.isEmpty()) {
            return false;
        } else {
            gsid_new = galaxy_scores.getLast().gsid;
            gs_iterator = galaxy_scores.iterator();
            return true;
        }
    }

    private void initializeNebulas() throws SQLException {
        if (connection == null || connection.isClosed()) {
            connection = SqlConnection.getInstance().getConnection();
            initStmt = connection.createStatement();
        }
        ResultSet rs;
        rs = initStmt.executeQuery("SELECT nid, d0, d1, d2 FROM nebula");
        nebulas = new HashMap<Integer, Nebula>();
        while (rs.next()) {
            Nebula nebula = Nebula.CreateNebula(rs);
            nebulas.put(nebula.nid, nebula);
        }
    }

    // process one score record, return false if there is no more score to be processed
    private boolean proceedPostScore() throws SQLException {
        if (!ps_iterator.hasNext()) {
            return false;
        }
        PostScore score = ps_iterator.next();
        User user = users.get(score.uid);
        Post post = posts.get(score.pid);
        User poster = users.get(post.uid);
        if (user == null) {
            System.out.println("ERROR: User " + score.uid + " is not found when processing score (psid) " + score.psid);
            return true;
        }
        if (post == null) {
            System.out.println("ERROR: Post " + score.uid + " is not found when processing score (psid) " + score.psid);
            return true;
        }
        if (poster == null) {
            System.out.println("ERROR: Poster " + score.uid + " is not found when processing score (psid) " + score.psid);
            return true;
        }
        int[] tag_pointer = post.nids;
        int[] nebula_X_tag = poster.nids;
        int[] nebula_Y_tag = user.nids;
        double[] nebula_X_PV = poster.profs;
        double[] nebula_Y_PV = user.profs;
        double[] nebula_Y_PVeff = Arrays.copyOf(user.profs, user.profs.length);
        double[] R = new double[tag_pointer.length];
        double[] ratio = new double[tag_pointer.length];
        double deltam = deltam(post);
        double m = 1 + score.score * deltam; //m is the professional weight
        calculate_pv(tag_pointer, nebula_X_tag, nebula_Y_tag, nebula_X_PV, nebula_Y_PV, nebula_Y_PVeff, R, ratio, deltam, m, score);
        return true;
    }
    // process one score record, return false if there is no more score to be processed

    private boolean proceedGalaxyScore() throws SQLException {
        if (!gs_iterator.hasNext()) {
            return false;
        }
        GalaxyScore score = gs_iterator.next();
        User user = users.get(score.uid);
        Galaxy galaxy = galaxies.get(score.gid);
        User poster = users.get(galaxy.uid);
        if (user == null) {
            System.out.println("ERROR: User " + score.uid + " is not found when processing score (psid) " + score.gsid);
            return true;
        }
        if (galaxy == null) {
            System.out.println("ERROR: Post " + score.uid + " is not found when processing score (psid) " + score.gsid);
            return true;
        }
        if (poster == null) {
            System.out.println("ERROR: Poster " + score.uid + " is not found when processing score (psid) " + score.gsid);
            return true;
        }
        int[] tag_pointer = galaxy.nids;
        int[] nebula_X_tag = poster.nids;
        int[] nebula_Y_tag = user.nids;
        double[] nebula_X_PV = poster.profs;
        double[] nebula_Y_PV = user.profs;
        double[] nebula_Y_PVeff = Arrays.copyOf(user.profs, user.profs.length);
        double[] R = new double[tag_pointer.length];
        double[] ratio = new double[tag_pointer.length];
        double deltam = deltam(galaxy);
        double m = 1 + score.score * deltam; //m is the professional weight
        calculate_pv(tag_pointer, nebula_X_tag, nebula_Y_tag, nebula_X_PV, nebula_Y_PV, nebula_Y_PVeff, R, ratio, deltam, m, score);
        return true;
    }

    private double deltam(Post post) {
        int pos = post.pos_count;
        int neg = post.neg_count;
        return deltam(pos, neg);
    }

    private double deltam(Galaxy galaxy) {
        int pos = galaxy.pos_count;
        int neg = galaxy.neg_count;
        return deltam(pos, neg);
    }

    private double deltam(int pos, int neg) {
        double deltam = 0;
        double dif_comment = Math.abs(pos - neg);
        if (dif_comment < 100) {
            deltam = 0.0025 * Math.PI * Math.sin(Math.PI * Math.abs(dif_comment) / 200);
        } else {
            deltam = 0.005 * Math.exp(-0.01 * Math.abs(dif_comment) / - 100);
        }
        return deltam;
    }

    private static String ProfToString(double[] profs) {
        int length = profs.length;
        String res = "";
        for (int i = 0; i < length; i++) {
            res += profs[i] + " ";
        }
        return res;
    }

    private void log_score() throws SQLException {
        if (connection == null || connection.isClosed()) {
            connection = SqlConnection.getInstance().getConnection();
            calStmt = connection.createStatement();
        }
        String query;
        for (User user : m_users) {
            query = "UPDATE user SET prof_vector = '" + ProfToString(user.profs) + "' WHERE uid = '" + user.uid + "'";
            calStmt.addBatch(query);
            System.out.println(query);
        }
        calStmt.executeBatch();

        if (psid_new > psid_old) {
            query = "INSERT INTO post_score_log SET psid = '" + psid_new + "'";
            calStmt.executeUpdate(query);
        }
        if (gsid_new > gsid_old) {
            query = "INSERT INTO galaxy_score_log SET gsid = '" + gsid_new + "'";
            calStmt.executeUpdate(query);
        }
        System.out.println("Done.");
    }

    @Override
    public void run() {
        try {
            System.out.print("#");
            if (connection == null || connection.isClosed()) {
                connection = SqlConnection.getInstance().getConnection();
                initStmt = connection.createStatement();
                calStmt = connection.createStatement();
            }
            if (!initializeData()) {
                connection.close();
                System.out.print("Scores: No need to re-calculating scores.");
                return;
            }
            if (psid_new > psid_old) {
                System.out.println("\nTrying to calculate post ratings from psid( " + psid_old + " ) to psid( " + psid_new + " )");
                while (proceedPostScore()) {
                }
            } else {
                System.out.println("Scores: No need to re-calculating post scores.");
            }
            if (gsid_new > gsid_old) {
                System.out.println("\nTrying to calculate galaxy ratings from gsid( " + gsid_old + " ) to gsid( " + gsid_new + " )");
                while (proceedGalaxyScore()) {
                }
            } else {
                System.out.println("Scores: No need to re-calculating galaxy scores.");
            }
            log_score();
            connection.close();
        } catch (SQLException ex) {
            Logger.getLogger(TimeoutUsers.class.getName()).log(Level.SEVERE, null, ex);
        }
    }

    private void calculate_pv(int[] tag_pointer, int[] nebula_X_tag, int[] nebula_Y_tag, double[] nebula_X_PV, double[] nebula_Y_PV, double[] nebula_Y_PVeff, double[] R, double[] ratio, double deltam, double m, Score score) {
        // Determine the relativity of each tagged Nebulas of the post
        for (int i = 0; i < tag_pointer.length; i++) {
            Nebula nebula = nebulas.get(tag_pointer[i]);
            boolean tagged = false;
            for (int j = 0; j < nebula_Y_tag.length; j++) {
                if (nebula_Y_tag[j] == tag_pointer[i]) {
                    tagged = true;
                    break;
                }
            }
            if (tagged) {
                R[i] = 1.0d;
                //nebula_Y_PVeff[ tag_pointer[i] ];
            } else if (nebula_Y_tag.length == 0) {
                return; // do nothing
            } else {
                // find the nearest nebula of the target nebula
                double min_distance = Double.MAX_VALUE;
                Nebula nearest = null;
                for (int j = 0; j < nebula_Y_tag.length; j++) {
                    Nebula nebula_y = nebulas.get(nebula_Y_tag[j]);
                    double distance = nebula_y.distance(nebula);
                    if (distance < min_distance) {
                        min_distance = distance;
                        nearest = nebula_y;
                    }
                }
                nebula_Y_PVeff[ tag_pointer[i]] = nebula_Y_PV[ nearest.nid];
                R[i] = Math.exp(-min_distance);
            }
        }

        // Determine the effective PV ratio between X(poster) and Y(rater)
        for (int i = 0; i < tag_pointer.length; i++) {
            if (nebula_Y_PVeff[ tag_pointer[i]] == 0) {
                ratio[i] = 0;
            } else {
                ratio[i] = nebula_Y_PVeff[ tag_pointer[i]] / nebula_X_PV[ tag_pointer[i]]; // what if nebula_X_PV[ tag_pointer[i] ] is zero ?
                //ratio correction
                if (ratio[i] >= 2) {
                    ratio[i] = 3 - Math.exp(2 - ratio[i]);
                } else if (ratio[i] <= 0.5) {
                    ratio[i] = 8 * Math.pow(ratio[i], 4);
                } else {
                    // do nothing
                }
            }
        }

        // Final Calculation
        for (int i = 0; i < tag_pointer.length; i++) {
            boolean tagged = false;
            for (int j = 0; j < nebula_X_tag.length; j++) {
                if (nebula_X_tag[j] == tag_pointer[i]) {
                    tagged = true;
                    break;
                }
            }
            if (tagged) {
                nebula_X_PV[ tag_pointer[i]] += ratio[i] * m * score.score * R[i];
            } else {
                nebula_X_PV[ tag_pointer[i]] += 0.5 * ratio[i] * m * score.score * R[i];
            }
            if (nebula_X_PV[ tag_pointer[i]] < 1) {
                nebula_X_PV[ tag_pointer[i]] = 1;
            }
        }
    }
}

class Nebula {

    public int nid = -1;
    public int d0 = 0;
    public int d1 = 0;
    public int d2 = 0;

    public Nebula(int n, int x, int y, int z) {
        nid = n;
        d0 = x;
        d1 = y;
        d2 = z;
    }

    public static Nebula CreateNebula(ResultSet rs) throws SQLException {
        int n = rs.getInt("nid");
        int d0 = rs.getInt("d0");
        int d1 = rs.getInt("d1");
        int d2 = rs.getInt("d2");
        return new Nebula(n, d0, d1, d2);
    }

    // return the Cartesian distance between two nebulas 
    public double distance(Nebula b) {
        double sum = 0;
        sum += (b.d0 - d0) * (b.d0 - d0);
        sum += (b.d1 - d1) * (b.d1 - d1);
        sum += (b.d2 - d2) * (b.d2 - d2);
        return Math.sqrt(sum);
    }
}

class User {

    public int uid = -1;
    public double[] profs = null;
    public int[] nids = new int[0];

    public User(int _uid, double[] _prof_vector, int[] ns) {
        uid = _uid;
        profs = _prof_vector;
        nids = ns;
    }

    public static User CreateUser(ResultSet rs) throws SQLException {
        int uid = rs.getInt("uid");
        String prof_vector = rs.getString("prof_vector");
        StringTokenizer st = new StringTokenizer(prof_vector, " ");
        int count = st.countTokens();
        double[] profs = new double[count];
        for (int i = 0; i < count; i++) {
            profs[i] = Double.valueOf(st.nextToken());
        }
        String tnids = rs.getString("nids");
        int[] nids = new int[0];
        if (tnids != null) {
            st = new StringTokenizer(rs.getString("nids"), ",");
            count = st.countTokens();
            nids = new int[count];
            for (int i = 0; i < count; i++) {
                nids[i] = Integer.valueOf(st.nextToken());
            }
        }
        return new User(uid, profs, nids);
    }
}

class Post {

    public int pid = -1;
    public int[] nids = null;
    public int uid = -1;
    public double t_score = 0;
    public int pos_count = 0;
    public int neg_count = 0;

    public Post(int _pid, int _uid, int[] _nids, double _t_score, int pos, int neg) {
        pid = _pid;
        uid = _uid;
        nids = _nids;
        t_score = _t_score;
        pos_count = pos;
        neg_count = neg;
    }

    public static Post CreatePost(ResultSet rs) throws SQLException {
        int pid = rs.getInt("pid");
        int uid = rs.getInt("uid");
        int pos = rs.getInt("pos_count");
        int neg = rs.getInt("neg_count");
        double t_score = rs.getDouble("t_score");
        String tnids = rs.getString("nids");
        int[] nids = new int[0];
        if (tnids != null) {
            StringTokenizer st = new StringTokenizer(tnids, ",");
            int count = st.countTokens();
            nids = new int[count + 1];
            for (int i = 0; i < count; i++) {
                nids[i] = Integer.valueOf(st.nextToken());
            }
            nids[count] = rs.getInt("default_nid");
        } else {
            nids = new int[]{rs.getInt("default_nid")};
        }
        return new Post(pid, uid, nids, t_score, pos, neg);
    }
}

class Galaxy {

    public int gid = -1;
    public int[] nids = null;
    public int uid = -1;
    public int pos_count = 0;
    public int neg_count = 0;

    public Galaxy(int _gid, int _uid, int[] _nids, int pos, int neg) {
        gid = _gid;
        uid = _uid;
        nids = _nids;
        pos_count = pos;
        neg_count = neg;
    }

    public static Galaxy CreateGalaxy(ResultSet rs) throws SQLException {
        int gid = rs.getInt("gid");
        int uid = rs.getInt("uid");
        int pos = rs.getInt("pos_count");
        int neg = rs.getInt("neg_count");
        String tnids = rs.getString("nids");
        int[] nids = new int[0];
        if (tnids != null) {
            StringTokenizer st = new StringTokenizer(tnids, ",");
            int count = st.countTokens();
            nids = new int[count];
            for (int i = 0; i < count; i++) {
                nids[i] = Integer.valueOf(st.nextToken());
            }
        }
        return new Galaxy(gid, uid, nids, pos, neg);
    }
}

class Score {

    public byte score = -1;
}

// represents a single score behaviour
class PostScore extends Score {

    public int uid = -1;
    public int pid = -1;
    public int psid = -1;

    public PostScore(int ps, int u, int p, byte s) {
        psid = ps;
        uid = u;
        pid = p;
        score = s;
    }

    public static PostScore CreateScore(ResultSet rs) throws SQLException {
        int psid = rs.getInt("psid");
        int pid = rs.getInt("pid");
        int uid = rs.getInt("uid");
        byte score = (byte) rs.getInt("score");
        return new PostScore(psid, uid, pid, score);
    }
}

class GalaxyScore extends Score {

    public int uid = -1;
    public int gid = -1;
    public int gsid = -1;

    public GalaxyScore(int gs, int u, int g, byte s) {
        gsid = gs;
        uid = u;
        gid = g;
        score = s;
    }

    public static GalaxyScore CreateScore(ResultSet rs) throws SQLException {
        int gsid = rs.getInt("gsid");
        int gid = rs.getInt("gid");
        int uid = rs.getInt("uid");
        byte score = (byte) rs.getInt("score");
        return new GalaxyScore(gsid, uid, gid, score);
    }
}
